<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    var countSpecialNumbers = function(n) { 
    
    let s = String(n), dp = Array.from({length:s.length}, () => Array(1<<10).fill(-1))
 
    /**
     * @param {number} i 从第i开始填数字
     * @param {number} mask 用位数据存储已填数字的集合
     * @param {boolean} isLimit 表示前面的数字是否已和n对应, 决定本次的上限是 s[i] 还是 9 
     * @param {boolean} isNum 表示前面是否已填; 如果是，就代表当前不会是前导0了, 后续可以从0开始; 
     *                           如果不是，那么可以跳过; 主要调节初始化和后续填值的不同
     * @return {number} 返回从i开始填的数字结果;  记忆化结果dp[i][mask] 如果已经不是-1 代表已经计算过 
     */
    const f = (i, mask, isLimit, isNum) =>{
         // 这次已经终点, 如果填过, 次数+1
         if(i === s.length) return isNum ? 1 : 0 
         
         // 如果不受限且前面已填且有记忆结果  直接返回
         if(!isLimit && isNum && dp[i][mask] >= 0) return  dp[i][mask]
         // 没有缓存 从0开始计算本次
         let res = 0 
         // 如果前面跳过没填 那么本次也跳过; 因为这样不可能达到上界,isLimit变为false
         if (!isNum) res = f(i + 1, mask, false, false)
         // d 是尝试要在当前位放置的数字, up代表数字的上界是多少
         for(let d = isNum ? 0 : 1 , up = isLimit ? +s[i] : 9 ; d <= up ; ++d ){
             // 如果mask记录里 没有这个数字  那就往下算 并计入结果; 
             // 下次的isLimit的前提是 本次已经被isLimit 且达到上限
             if((mask >> d & 1)=== 0 )  res += f(i + 1, mask | (1 << d) , isLimit && d === up, true )      
         }
         // 如果自由不受限 且本次已填过 计入cache
         if(!isLimit && isNum) dp[i][mask] = res 
       
         return res 
    }  
    // 从第0位开始, mask也是空, 受限, 前面没填过 
    return f(0, 0,  true,  false)
 };
  </script>
</body>
</html>